﻿// ******************************************************
// Copyright © 2010 DCOM Productions®, 
// All Rights Reserved
// ******************************************************

namespace BitFlex.EnterpriseServices.Authentication {
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Data.SqlClient;
    using System.Data;
    using System.Security.Cryptography;

    /// <summary>
    /// Performs basic enterprise level authentication against an sql datasource
    /// </summary>
    /// <typeparam name="TUser">A user based on <see cref="ISecureUser"/> used to perform authentication</typeparam>
    public class SqlAuthentication<TUser> : IAuthenticationService<TUser> where TUser : ISecureUser, new() {
        /// <summary>
        /// Initializes a new instance of the BitFlex.EnterpriseServices.Authentication.SqlAuthentication class
        /// </summary>
        /// <param name="connectionString">The connection string used to connect to the datasourceS</param>
        public SqlAuthentication(string connectionString) {
            m_ConnectionString = connectionString;
            DataSourceMap = new SqlAuthenticationTableMap();
        }

        #region Properties

        private string m_ConnectionString = string.Empty;
        /// <summary>
        /// Gets or sets the connection string used to connect to the datasource
        /// </summary>
        public string ConnectionString {
            get {
                return m_ConnectionString;
            }
            set {
                m_ConnectionString = value;
            }
        }

        /// <summary>
        /// Gets or sets the sql mapping for the table that is used to store users
        /// </summary>
        public SqlAuthenticationTableMap DataSourceMap {
            get;
            set;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Maps the sql authentication service to the specified table and columns in the sql datasource
        /// </summary>
        /// <param name="tableName">The name of the sql table to map</param>
        /// <param name="nameColumn">The name of the sql column that stores the user's name to map</param>
        /// <param name="passwordColumn">The name of the sql column that stores the user's password to map</param>
        public void MapDataSource(string tableName, string nameColumn, string passwordColumn) {
            DataSourceMap.TableName = tableName;
            DataSourceMap.NameColumn = nameColumn;
            DataSourceMap.PasswordColumn = passwordColumn;
        }

        private bool ContainsUser(DataTable table, string username) {
            foreach (DataRow row in table.Rows) {
                if (row.ItemArray.Count() > 0) {
                    if (row[DataSourceMap.NameColumn] != null) {
                        if (row[DataSourceMap.NameColumn].ToString().ToLower() == username.ToLower()) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

        private TUser GetUser(DataTable table, string username) {
            TUser user = new TUser();
            foreach (DataRow row in table.Rows) {
                if (row.ItemArray.Count() > 0) {
                    if (row[DataSourceMap.NameColumn] != null) {
                        if (row[DataSourceMap.NameColumn].ToString().ToLower() == username.ToLower()) {
                            user.Name = (string)row[DataSourceMap.NameColumn];
                            user.Password = (string)row[DataSourceMap.PasswordColumn];
                            return user;
                        }
                    }
                }
            }
            return default(TUser);
        }

        #endregion

        #region IAuthenticationService<TUser> Members

        private Encoding m_Encoding = Encoding.ASCII;
        /// <summary>
        /// Gets or sets the encoding used when altering passwords during encryption
        /// </summary>
        public Encoding Encoding {
            get {
                return m_Encoding;
            }
            set {
                m_Encoding = value;
            }
        }

        private EncryptionType m_Encryption = EncryptionType.Raw;
        /// <summary>
        /// Gets or sets the type of enterprise encryption that will be used when sending passwords
        /// </summary>
        public EncryptionType Encryption {
            get {
                return m_Encryption;
            }
            set {
                m_Encryption = value;
            }
        }

        /// <summary>
        /// Authenticates the specified user against the sql datasource
        /// </summary>
        /// <param name="user">The user to authenticate</param>
        /// <returns>Returns a <see cref="AuthenticationResult"/> enumeration</returns>
        public AuthenticationResult Authenticate(TUser user) {
            using (SqlConnection connection = new SqlConnection(this.ConnectionString)) {
                try {
                    connection.Open();
                }
                catch (System.InvalidOperationException) {
                    return AuthenticationResult.Error;
                }
                catch (System.Data.SqlClient.SqlException) {
                    return AuthenticationResult.NetworkError;
                }
                string query = string.Format("SELECT {0}, {1} FROM dbo.{2} WHERE LOWER({0}) = '{3}'",
                    DataSourceMap.NameColumn, 
                    DataSourceMap.PasswordColumn, 
                    DataSourceMap.TableName,
                    user.Name.ToLower());
                using (SqlDataAdapter adapter = new SqlDataAdapter(query, connection)) {
                    DataTable table = new DataTable(DataSourceMap.TableName);
                    adapter.Fill(table);
                    if (ContainsUser(table, user.Name)) {
                        TUser remoteUser = GetUser(table, user.Name);
                        if (remoteUser.Password == EncryptPassword(user.Password)) {
                            return AuthenticationResult.Success;
                        }
                        else {
                            return AuthenticationResult.IncorrectPassword;
                        }
                    }
                    else {
                        return AuthenticationResult.UserNotFound;
                    }
                }
            }
        }

        /// <summary>
        /// Encrypts the specified password using Enterpise level encryption
        /// </summary>
        /// <param name="password">The password to encrypt</param>
        /// <returns>Returns the fully encrypted password</returns>
        public string EncryptPassword(string password) {
            switch (this.Encryption) {
                case EncryptionType.Md5:
                    return EncryptionHelper.GetMd5EncryptedPassword(this.Encoding, password);
            }
            return password; // BitFlex.EnterpriseServices.Authentication.EncryptionType.Raw
        }

        #endregion
    }
}